#include "config.h"

#include "ms2lstat.h"

#define CONTIG_THRESHOLD	2

#define DIFF(a, b)	(((a) >= (b)) ? ((a) - (b)): ((b) - (a)))

ecm_hashtable_t	hash_session;
static ecm_ptrs_t	*ptrs_session;

static ecm_uint64	total_sesslen;

static hist_t	*hist_sesslen_normal;
static hist_t	*hist_sesslen_ratio;
static hist_t	*hist_jumpop;
static hist_t	*hist_jumpdist_all;
static hist_t	*hist_jumpdist_forward;
static hist_t	*hist_jumpdist_backward;
static hist_t	*hist_interseek_normal;
static hist_t	*hist_interseek_ratio;

ecm_uint32	session_gap = 5;

typedef struct {
	ecm_uint32	mid;
	ecm_uint32	cid;
} hkey_t;

typedef struct {
	ecm_uint32	n_accesses;
	ecm_uint32	n_forwards;
	ecm_uint32	n_backwards;
} access_anal_t;

static void
free_access(access_t *access)
{
	list_del_init(&access->list);
	ecm_free(access);
}

static access_t *
create_access(session_t *session, ecm_uint64 tick, ecm_uint32 startstamp, ecm_uint32 duration, ecm_int32 playback_rate)
{
	access_t	*access;
	media_t		*media = get_media(session->mid);

	access = (access_t *)ecm_malloc(sizeof(access_t));
	access->tick_start = (ecm_uint32)tick;
	access->startstamp = startstamp;
	access->playback_rate = playback_rate;

	if (playback_rate > 0) {
		if (media->length < startstamp + duration)
			duration = (ecm_uint32)(media->length - startstamp);
	}
	else {
		if (startstamp < duration)
			duration = startstamp;
	}

	if (playback_rate == 1) {
		access->tick_end = access->tick_start + duration;
		access->endstamp = startstamp + duration;
	}
	else {
		access->tick_end = access->tick_start + duration / playback_rate;

		if (playback_rate > 1)
			access->endstamp = startstamp + duration;
		else {
			if (startstamp < duration)
				access->endstamp = 0;
			else
				access->endstamp = startstamp - duration;
		}
	}

	INIT_LIST_HEAD(&access->list);

	ECMASSERT(access->tick_start <= access->tick_end);

	return access;
}

static access_t *
create_access_with_lent(session_t *session, lent_t *lent)
{
	return create_access(session, lent->tick, lent->startstamp, lent->duration, lent->playback_rate);
}

static access_t *
find_last_access(session_t *session)
{
	ECMASSERT(!list_empty(&session->accesses));

	return list_entry(session->accesses.prev, access_t, list);
}

static void
shrink_access(access_t *access, ecm_uint32 tick_end)
{
	access->tick_end = tick_end;
	access->endstamp = access->startstamp + (access->tick_end - access->tick_start) * access->playback_rate;
}

static access_t *
get_normal_access(session_t *session, lent_t *lent)
{
	access_t	*access, *access_last;

again:
	if (list_empty(&session->accesses)) {
		access = create_access_with_lent(session, lent);
		list_add(&access->list, &session->accesses);
		add_arrival(session);
		return access;
	}

	access_last = find_last_access(session);

	if (DIFF(access_last->tick_end, lent->tick) <= CONTIG_THRESHOLD) {
		access = create_access(session, access_last->tick_end,
				       lent->startstamp,
				       lent->duration,
				       lent->playback_rate);
	}
	else {
		if (access_last->tick_end > lent->tick) {
			if (access_last->tick_start > lent->tick) {
				free_access(access_last);

				if (list_empty(&session->accesses))
					del_arrival(session);
				goto again;
			}
			else {
				shrink_access(access_last, lent->tick);
			}
		}
		access = create_access_with_lent(session, lent);
	}

	list_add(&access->list, &access_last->list);

	return access;
}

static ecm_double
get_interactivity(void)
{
	session_t	*session;
	ecm_uint32	n_interactivities = 0;
	ecm_uint32	i;

	for (i = 0; i < ptrs_session->len; i++) {
		session = (session_t *)ptrs_session->pdata[i];
		if (session->accesses.next != session->accesses.prev)
			n_interactivities++;
	}
	return ((ecm_double)n_interactivities / ptrs_session->count);
}

static session_t *
create_session(lent_t *lent)
{
	session_t	*session;
	ecm_uint32	mid = lent->mid;
	ecm_uint32	cid = lent->cid;
	hkey_t	hkey = { mid, cid };

	session = (session_t *)ecm_malloc(sizeof(session_t));
	session->sid = ecm_ptrs_add(ptrs_session, session);
	session->mid = mid;
	session->cid = cid;

	INIT_LIST_HEAD(&session->accesses);
	INIT_LIST_HEAD(&session->list_ia);

	ecm_hashtable_insert(hash_session, session, sizeof(hkey_t), &hkey);

	return session;
}

static session_t *
get_session(lent_t *lent)
{
	session_t	*session;
	ecm_uint32	mid = lent->mid;
	ecm_uint32	cid = lent->cid;
	access_t	*access_last;
	hkey_t	hkey = { mid, cid };

	session = (session_t *)ecm_hashtable_get(hash_session, sizeof(hkey_t), &hkey);
	if (session == NULL) {
		return create_session(lent);
	}

	access_last = find_last_access(session);
	if (access_last != NULL && lent->tick - access_last->tick_end > session_gap) {
		ecm_hashtable_remove(hash_session, sizeof(hkey_t), &hkey);
		return create_session(lent);
	}
	return session;
}

static void
add_access(session_t *session, lent_t *lent)
{
#ifdef MSM_DEBUG
	if (lent->playback_rate == 1)
		DBG(DBG_NORMAL_LOG, "log: %s", dbg_lent(lent, session));
	else if (lent->playback_rate > 1)
		DBG(DBG_FORWARD_LOG, "log: %s", dbg_lent(lent, session));
	else
		DBG(DBG_BACKWARD_LOG, "log: %s", dbg_lent(lent, session));
#endif

	get_normal_access(session, lent);
}

void
update_session(lent_t *lent)
{
	session_t	*session;

	if (lent->status != 200)
		return;

	if (lent->duration == 0)
		return;

	session = get_session(lent);

	add_access(session, lent);
}

static void
analyze_session_access(session_t *session, access_anal_t *panal)
{
	access_t	*access, *access_prev = NULL;
	media_t		*media;
	ecm_uint32	mlength;
	struct list_head	*lp;

	ecm_memset(panal, 0, sizeof(access_anal_t));
	media = get_media(session->mid);
	mlength = (ecm_uint32)media->length;

	list_for_each (lp, &session->accesses) {
		access = list_entry(lp, access_t, list);

		panal->n_accesses++;
		if (access->playback_rate > 1)
			panal->n_forwards++;
		else if (access->playback_rate < 0)
			panal->n_backwards++;

		if (access_prev != NULL) {
			ecm_uint32	jumpdist;

			jumpdist = DIFF(access_prev->endstamp, access->startstamp);
			ECMASSERT(mlength >= jumpdist);
			jumpdist = (jumpdist * 100) / mlength;
			if (jumpdist > 0) {
				add_hist(hist_jumpdist_all, jumpdist, 1);
				if (access_prev->endstamp < access->startstamp)
					add_hist(hist_jumpdist_forward, jumpdist, 1);
				else
					add_hist(hist_jumpdist_backward, jumpdist, 1);
			}
			if (access_prev->playback_rate == 1) {
				ecm_uint32	playtime = access_prev->tick_end - access_prev->tick_start;
				add_hist(hist_interseek_normal, playtime, 1);
				add_hist(hist_interseek_ratio, (playtime * 100) / mlength, 1);
			}
		}
		access_prev = access;
	}
}

ecm_double
get_interseek_time(session_t *session)
{
	ecm_uint32	total_playtime = 0;
	access_t	*access, *access_prev = NULL;
	media_t	*media;
	ecm_uint32	mlength;
	ecm_uint32	count = 0;
	struct list_head	*lp;

	media = get_media(session->mid);
	mlength = (ecm_uint32)media->length;

	list_for_each (lp, &session->accesses) {
		access = list_entry(lp, access_t, list);
		if (access_prev != NULL) {
			ecm_uint32	playtime = access_prev->tick_end - access_prev->tick_start;

			total_playtime += playtime;
			count++;
		}
		access_prev = access;
	}

	if (total_playtime == 0)
		return 0;
	return ((ecm_double)total_playtime / count / mlength) * 100;
}

ecm_uint32
get_sesslen(session_t *session)
{
	ecm_uint32	sesslen = 0;
	access_t	*access;
	struct list_head	*lp;

	list_for_each (lp, &session->accesses) {
		access = list_entry(lp, access_t, list);
		ECMASSERT(access->tick_start <= access->tick_end);
		sesslen += (access->tick_end - access->tick_start);
	}

	return sesslen;
}

static void
insert_session_length(session_t *session)
{
	media_t		*media;
	ecm_uint32	sesslen;

	media = get_media(session->mid);
	ECMASSERT(!list_empty(&session->accesses));

	sesslen = get_sesslen(session);

	add_hist(hist_sesslen_normal, sesslen, 1);
	total_sesslen += sesslen;

	if (media->length > 0 && sesslen > 0)
		add_hist(hist_sesslen_ratio, (sesslen * 100) / media->length, 1);
}

static void
report_session(FILE *fp_sess, session_t *session)
{
	access_t	*access;
	struct list_head	*lp;

	ecm_fprintf(fp_sess, "%6u %6u %6u %u ", session->sid, session->mid, session->cid, get_media_rank(session->mid));
	{
		access_t	*first, *last;

		first = list_entry(session->accesses.next, access_t, list);
		last = list_entry(session->accesses.prev, access_t, list);
		ecm_fprintf(fp_sess, "%u-%u ", first->tick_start, last->tick_end);
	}

	list_for_each (lp, &session->accesses) {
		access = list_entry(lp, access_t, list);
		ecm_fprintf(fp_sess, "%u-%u:%u-%u:%d ",
			    access->tick_start, access->tick_end,
			    access->startstamp, access->endstamp, access->playback_rate);
	}
	ecm_fprintf(fp_sess, "\n");
}

static void
report_session_infos(void)
{
	ecm_uint32	i;
	session_t	*session;
	FILE	*fp_sess = setup_logging("session");

	ecm_fprintf(fp_sess, "# sid mid cid mid_rank start_tick-end_tick tick:start-end\n");

	for (i = 0; i < ptrs_session->len; i++) {
		access_anal_t	anal;

		session = (session_t *)ptrs_session->pdata[i];
		report_session(fp_sess, session);

		if (stat_mid > 0 && session->mid != stat_mid)
			continue;
		if (stat_media_rank > 0) {
			ecm_uint32	rank = get_media_rank(session->mid);

			if (rank > stat_media_rank)
				continue;
		}
		analyze_session_access(session, &anal);

		insert_session_length(session);
		add_hist(hist_jumpop, anal.n_accesses, 1);
	}

	ecm_fclose(fp_sess);
}

static void
report_sesslen_normal_hist(const char *name, hist_t *hist_sesslen)
{
	FILE	*fp_sesslen, *fp_sesslen_cdf;
	char	*fname;
	ecm_uint32	sum = 0;
	ecm_double	accu_prob = 0;
	ecm_uint32	i;

	fp_sesslen = setup_logging(name);
	ecm_asprintf(&fname, "%s_cdf", name);
	fp_sesslen_cdf = setup_logging(fname);
	ecm_free(fname);

	for (i = 0; i <= hist_sesslen->n_max; i++) {
		sum += hist_sesslen->data[i];
	}

	ecm_fprintf(fp_sesslen, "# %s PDF\n", name);
	ecm_fprintf(fp_sesslen_cdf, "# %s CDF\n", name);

	for (i = 0; i <= hist_sesslen->n_max; i++) {
		if (hist_sesslen->data[i] > 0) {
			ecm_double	prob = ((ecm_double)hist_sesslen->data[i]) / sum;

			accu_prob += prob;
			ecm_fprintf(fp_sesslen, "%8u %9.8f\n", i + 1, prob);
			ecm_fprintf(fp_sesslen_cdf, "%8u %9.8f\n", i + 1, accu_prob);
		}
	}

	ecm_fclose(fp_sesslen);
	ecm_fclose(fp_sesslen_cdf);
}

static void
report_sesslen_ratio_hist(const char *name, hist_t *hist_sesslen)
{
	FILE	*fp_sesslen, *fp_sesslen_cdf;
	FILE	*fp_sesslen_full, *fp_sesslen_full_cdf;
	char	*fname;
	ecm_uint32	sum = 0, sum_full = 0;
	ecm_double	accu_prob = 0;
	ecm_uint32	i;

	fp_sesslen = setup_logging(name);
	ecm_asprintf(&fname, "%s_cdf", name);
	fp_sesslen_cdf = setup_logging(fname);
	ecm_free(fname);

	ecm_asprintf(&fname, "%s_full", name);
	fp_sesslen_full = setup_logging(fname);
	ecm_free(fname);
	ecm_asprintf(&fname, "%s_full_cdf", name);
	fp_sesslen_full_cdf = setup_logging(fname);
	ecm_free(fname);

	for (i = 0; i <= hist_sesslen->n_max; i++) {
		sum_full += hist_sesslen->data[i];
		if (i <= 100) {
			sum += hist_sesslen->data[i];;
		}
	}

	ecm_fprintf(fp_sesslen, "# %s PDF\n", name);
	ecm_fprintf(fp_sesslen_cdf, "# %s CDF\n", name);
	ecm_fprintf(fp_sesslen_full, "# %s_full PDF\n", name);
	ecm_fprintf(fp_sesslen_full_cdf, "# %s_full CDF\n", name);

	for (i = 0; i <= hist_sesslen->n_max; i++) {
		if (hist_sesslen->data[i] > 0) {
			ecm_double	prob = ((ecm_double)hist_sesslen->data[i]) / sum;

			accu_prob += prob;
			ecm_fprintf(fp_sesslen_full, "%8u %9.8f\n", i, prob);
			ecm_fprintf(fp_sesslen_full_cdf, "%8u %9.8f\n", i, accu_prob);

			if (i <= 100) {
				ecm_fprintf(fp_sesslen, "%8u %9.8f\n", i, prob);
				ecm_fprintf(fp_sesslen_cdf, "%8u %9.8f\n", i, accu_prob);
			}
		}
	}

	ecm_fclose(fp_sesslen_full);
	ecm_fclose(fp_sesslen_full_cdf);
	ecm_fclose(fp_sesslen);
	ecm_fclose(fp_sesslen_cdf);
}

static void
report_jumpop_hist(void)
{
	FILE	*fp_jumpop = setup_logging("jumpop");
	ecm_uint32	i;

	ecm_fprintf(fp_jumpop, "# jumpop count\n");

	for (i = 1; i <= hist_jumpop->n_max; i++) {
		if (hist_jumpop->data[i] > 0)
			ecm_fprintf(fp_jumpop, "%u %u\n", i, hist_jumpop->data[i]);
	}

	ecm_fclose(fp_jumpop);
}

static void
report_interseek_normal_hist(void)
{
	FILE	*fp_is = setup_logging("interseek_normal");
	ecm_uint32	i;

	ecm_fprintf(fp_is, "# interseek count\n");

	for (i = 1; i <= hist_interseek_normal->n_max; i++) {
		if (hist_interseek_normal->data[i] > 0)
			ecm_fprintf(fp_is, "%8u %u\n", i, hist_interseek_normal->data[i]);
	}

	ecm_fclose(fp_is);
}

static void
report_interseek_ratio_hist(void)
{
	FILE	*fp_is = setup_logging("interseek_ratio");
	ecm_uint32	i;

	ecm_fprintf(fp_is, "# interseek_ratio prob\n");

	for (i = 0; i <= hist_interseek_ratio->n_max; i++) {
		if (hist_interseek_ratio->data[i] > 0)
			ecm_fprintf(fp_is, "%8u %lf\n", i, ((ecm_double)hist_interseek_ratio->data[i]) / hist_interseek_ratio->n_count);
	}

	ecm_fclose(fp_is);
}

static void
report_jumpdist_hist(const char *name, hist_t *hist_jumpdist)
{
	FILE	*fp_jumpdist = setup_logging(name);
	ecm_uint32	i;

	ecm_fprintf(fp_jumpdist, "# total count: %d\n", hist_jumpdist->n_count);
	ecm_fprintf(fp_jumpdist, "# %s count ratio\n", name);

	for (i = 1; i <= hist_jumpdist->n_max; i++) {
		ecm_uint32	count = hist_jumpdist->data[i];

		if (count > 0)
			ecm_fprintf(fp_jumpdist, "%u %u %lf\n", i, count, (ecm_double)count / hist_jumpdist->n_count);
	}

	ecm_fclose(fp_jumpdist);
}

void
report_session_result(FILE *fp)
{
	ecm_double	avg_sesslen;

	ecm_fprintf(fp, "Total Number of Sessions:           %u\n", ptrs_session->count);
	ecm_fprintf(fp, "Session Interactivity:              %lf\n", get_interactivity());

	hist_sesslen_normal = create_hist();
	hist_sesslen_ratio = create_hist();
	hist_jumpop = create_hist();
	hist_jumpdist_all = create_hist();
	hist_jumpdist_forward = create_hist();
	hist_jumpdist_backward = create_hist();
	hist_interseek_normal = create_hist();
	hist_interseek_ratio = create_hist();

	report_session_infos();
	report_sesslen_normal_hist("sesslen_normal", hist_sesslen_normal);
	report_sesslen_ratio_hist("sesslen_ratio", hist_sesslen_ratio);
	report_jumpop_hist();
	report_interseek_normal_hist();
	report_interseek_ratio_hist();
	report_jumpdist_hist("jumpdist_all", hist_jumpdist_all);
	report_jumpdist_hist("jumpdist_forward", hist_jumpdist_forward);
	report_jumpdist_hist("jumpdist_backward", hist_jumpdist_backward);

	free_hist(hist_sesslen_normal);
	free_hist(hist_sesslen_ratio);
	free_hist(hist_jumpop);
	free_hist(hist_jumpdist_all);
	free_hist(hist_jumpdist_forward);
	free_hist(hist_jumpdist_backward);
	free_hist(hist_interseek_normal);
	free_hist(hist_interseek_ratio);

	avg_sesslen = ((ecm_double)total_sesslen) / ptrs_session->count;
	ecm_fprintf(fp, "Avg Session Length:                 %.2lf\n", avg_sesslen);
	ecm_fprintf(fp, "Max Hourly Sessions(Estimated):     %.2lf\n",
		    (avg_sesslen * max_hourly_n_arrivals) / 3600);
}

ECMBOOL
init_session(void)
{
	hash_session = ecm_hashtable_create(256);
	ptrs_session = ecm_ptrs_new(128);

	return ECM_TRUE;
}
